查看原文
其他

VT虚拟化技术笔记

smallzhong_ 看雪学苑 2022-07-01


本文为看雪论坛优秀文章
看雪论坛作者ID:smallzhong_



VT技术笔记(part 1)


最近在学习VT技术,想把学习过程中记录的笔记分享出来。技术不精有不对的地方还望指正。代码会发到https://github.com/smallzhong/myvt这个仓库中,目前还在施工中,还没写完。欢迎star,预计这个月内完工。



VT概述

VT技术的大致含义如上。一个CPU上有vm monitor和guest。在guest中感受不到自己跑在虚拟环境中。执行普通的操作时在guest中的操作和未开启vt时的操作并没有什么不同,但是在执行一些特殊的指令或者进行一些特殊的操作,如执行cpuid指令、切换cr3指令、读写msr寄存器指令、触发异常时,会将控制权交还给vm monitor,通过vm monitor确定应该如何让该指令或者操作得到执行。

在云服务器的搭建中可以使用该技术让一台实体机上可以跑多台虚拟机。而该技术也可以做到无痕对一些指令进行高权限的hook。甚至可以通过ept技术进行对指定地址的无痕hook。



VT执行的大体流程


该流程的描述出自B站周壑的VT教学视频中,讲得非常通俗易懂。VT的开启与关闭大致分为开锁、开柜门、拔电源、选中机器、装机、开机、拔电源、关柜门、关锁9个步骤。这9个步骤的具体描述如下。此处为大体的描述,在后面会对每一个步骤的具体细节进行具体的说明。

① 开锁:检测是否支持VT。

② 开柜门:vmxon。需要申请一块内存然后把这块内存的lowpart和highpart作为参数调用vmxon。注意这块内存的开头要填入一个特定的msr寄存器中的值。注意这里vmxon修改cr4寄存器之后,cr0寄存器的页保护和段保护位都不能再置位,如果试图修改cr0的页保护或者段保护位,会触发异常蓝屏。

③ 拔电源:vmclear。

④ 选中机器:vmptrload,相当于一个指针。需要通过vmptrload来指向某个机器。如果当前有多个机器的话,在各个机器之间的切换操作也需要用到vmptrload。

⑤ 装机:设置vmcs。也是要申请一块内存,然后在这块内存里面填入开启虚拟机时虚拟机使用的一些寄存器。要注意这里不能直接操作这块内存区域,要用vmwrite来操作。因为随着版本的更新,原来在这块内存区域的东西可能后来就不在了。vmwrite保证兼容性。

⑥ 开机vmlaunch。

⑦ 拔电源vmclear。

⑧ 关柜门vmoff。


检测是否支持VT(开锁)


在intel手册31.5章中详细描述了vmm的开启过程。


① 通过cpuid指令查询CPU是否支持VT。

② 通过一些特定的MSR寄存器(在31.5.1中有介绍)来确认是否支持开启VT。

③ 申请一块非分页内存,用来存放vmxon区域。其大小在IA32_VMX_BASIC这个寄存器中指定。该内存要4kb对齐(后12位全为0)。实际申请时申请一个页的大小的非分页内存即可。

④ 初始化vmxon区域的版本编号(前4个字节)。版本编号通过IA32_VMX_BASIC这个寄存器的低4字节获取。实际实验时发现这个数为1,理论上把上一步申请到的内存前四字节置1即可,但为了兼容最好还是从寄存器中取值出来。

⑤ 给cr0和cr4寄存器的特定位置置位,使得其满足如下要求:

IA32_VMX_CR0_FIXED0 寄存器中为1的位,在cr0中必须为1;

IA32_VMX_CR0_FIXED1 寄存器中为0的位,在cr0中必须为0;

IA32_VMX_CR4_FIXED0 寄存器中为1的位,在cr4中必须为1;

IA32_VMX_CR4_FIXED1 寄存器中为0的位,在cr4中必须为0。


⑥ 确认IA32_FEATURE_CONTROL寄存器被正确置位。该寄存器的具体细节可以通过《处理器虚拟化技术》2.3.2.1找到。


也就是说,该寄存器的bit0是lock为,bit2是outside位。必须保证这两位均为1才可以开启VT。可以大致通过读取该寄存器后将其&5,判断是否两个位均为1。

⑦ 执行vmxon指令,传进去的参数是一个指向存放vmxon区域的物理地址的指针。如果该指令成功,那么rflags.cf=0,否则rflags.cf=1。

由上可写出检测是否支持VT的代码如下:
//检测Bios是否开启VTBOOLEAN VmxIsCheckSupportVTBIOS(){ ULONG64 value = __readmsr(IA32_FEATURE_CONTROL);
return (value & 0x5) == 0x5;}

//检测CPU是否支持VTBOOLEAN VmxIsCheckSupportVTCPUID(){ int cpuidinfo[4]; __cpuidex(cpuidinfo, 1, 0); //CPUID 是否支持VT ecx.vmx第6位 如果为1,支持VT,否则不支持 return (cpuidinfo[2] >> 5) & 1;}

//检测CR4VT是否开启,如果为1 代表已经开启过了,否则没有开启BOOLEAN VmxIsCheckSupportVTCr4(){ ULONG64 mcr4 = __readcr4(); //检测CR4 VT是否开启,cr4.vmxe如果第14位为1,那么VT已经被开启,否则可以开启 return ((mcr4 >> 13) & 1) == 0;}
void checkVT(){ if (VmxIsCheckSupportVTCPUID()) { DbgPrintEx(77, 0, "[db]:VmxIsCheckSupportVTCPUID number = %d\r\n", KeGetCurrentProcessorNumber()); }
if (VmxIsCheckSupportVTBIOS()) { DbgPrintEx(77, 0, "[db]:VmxIsCheckSupportVTBIOS number = %d\r\n", KeGetCurrentProcessorNumber()); }
if (VmxIsCheckSupportVTCr4()) { DbgPrintEx(77, 0, "[db]:VmxIsCheckSupportVTCr4 number = %d\r\n", KeGetCurrentProcessorNumber()); }}



vmxon(开柜门)


申请一块4KB对齐的非分页内存,作为vmxon的参数。
PHYSICAL_ADDRESS lowphys,heiPhy;lowphys.QuadPart = 0;heiPhy.QuadPart = -1;pVcpu->VmxOnAddr = MmAllocateContiguousMemorySpecifyCache(PAGE_SIZE, lowphys, heiPhy, lowphys, MmCached);

设置vmxon区域。这一块的说明在intel白皮书24.11.5中。这一块内存是操作系统使用的。在申请区域之后要rtlzeromemory一下,防止没有挂页。其他位都置零即可,之后操作系统进行虚拟机的管理的时候会使用到这块内存。

我们需要设置的只有头四个字节。要把msr中 IA32_VMX_BASIC 这个寄存器的低4字节填入vmxon的头4字节。否则vmxon的执行会出错。基本上这个寄存器的低4字节会是1。之后随着版本的更新可能会有变化。
ULONG64 vmxBasic = __readmsr(IA32_VMX_BASIC);*(PULONG)pVcpu->VmxOnAddr = (ULONG)vmxBasic;

在设置完vmxon区域之后,还需最后一步即可vmxon。需要根据msr寄存器中的指示进行cr0和cr4寄存器的设置。


具体解释在附录A中。


简单解释就是在进行vmxon之前, IA32_VMX_CR0_FIXED0 寄存器中为1的值,在cr0中必须为1、 IA32_VMX_CR0_FIXED1 寄存器中为0的值,在cr0中必须为0。cr4同理。

那么可以通过 cr0 = cr0 | IA32_VMX_CR0_FIXED0 & IA32_VMX_CR0_FIXED1 操作将cr0和cr4进行置位。具体代码如下:
ULONG64 vcr00 = __readmsr(IA32_VMX_CR0_FIXED0);ULONG64 vcr01 = __readmsr(IA32_VMX_CR0_FIXED1);ULONG64 vcr04 = __readmsr(IA32_VMX_CR4_FIXED0);ULONG64 vcr14 = __readmsr(IA32_VMX_CR4_FIXED1);
ULONG64 mcr4 = __readcr4();ULONG64 mcr0 = __readcr0();
mcr4 |= vcr04;mcr4 &= vcr14;mcr0 |= vcr00;mcr0 &= vcr01;
__writecr0(mcr0);__writecr4(mcr4);

在进行以上步骤之后,可以通过vmxon打开柜门。
int error = __vmx_on(&pVcpu->VmxOnAddrPhys.QuadPart);


申请vmcs内存并vmclear(拔电源)


首先申请vmcs内存。和vmxon内存类似,这一块内存的头4个字节也要填入 IA32_VMX_BASIC 的低4个字节。vmcs中保存的是虚拟机的各种寄存器和控制区域、vmexit控制区域等。在装机过程中要通过vmwrite进行vmcs的设置。

申请vmcs内存并设置头4字节为 IA32_VMX_BASIC 的低4字节之后,通过vmclear指令进行初始化。传入的参数与vmxon的参数类似,是指向申请的内存的物理地址的一个指针。vmclear的用法如下:
__vmx_vmclear(&pVcpu->VmxcsAddrPhys.QuadPart);


vmptrload(选中机器)


vmptrload 指令和vmxon、vmclear类似,传入的是指向vmcs结构的物理地址的一个指针。只有执行了这行指令之后才能通过 vmwrite 进行vmcs结构的填写。代码如下:
__vmx_vmptrld(&pVcpu->VmxcsAddrPhys.QuadPart);


设置vmcs(装机)


24章中描述了需要设置的VMCS字段。大体包括如下几部分:

Guest state fields

Host state fields

vm-control fields
  • vm execution control

  • vm exit control

  • vm entry control


除这5个区域之外,vmcs还有一个区域是vm exit信息区域。这个区域是只读的,存储的是vmx指令失败后失败代码的编号。

VMCS字段的设置是VT中最为繁琐的一步。对于VMCS中一些具体字段应该如何进行设置,会在下一篇文章中进行说明。



参考文献

intel白皮书

邓志《处理器虚拟化技术》

B站周壑VT教学视频



VT虚拟化技术笔记(part 2)


最近在学习VT技术,想把学习过程中记录的笔记分享出来。技术不精,有不对的地方还望指正。代码会发到https://github.com/smallzhong/myvt这个仓库中,目前还没写完。


设置vmcs控制区的guest和host区域


本文讲解vmcs控制区中Guest state fields和Host state fields字段的填充方法,vm-control fields的相关填充方法将会在下一篇文章中讲解。


在intel白皮书的24.3章中,详细描述了VMCS控制区的字段。如上一篇文章所说,需要设置的vmcs字段为:

Guest state fields,在VT从虚拟机中退出时,处理器的状态(寄存器等)会被存储在该区域中。而进入虚拟机(开启VT)时,虚拟机中的各种处理器的状态都由进入虚拟机时该区域的对应字段的值来决定。

Host state fields,在从虚拟机中退出时,host对CPU进行接管。host接管后各种寄存器的状态存储在这个区域。也就是说当虚拟机中发生了vm-exit事件,CPU会从guest返回到host,这个区域中的值会被设置到对应的寄存器中,然后按照设置完之后的eip继续执行。

vm-control fields
  • vm execution control

  • vm exit control

  • vm entry control


除这5个区域之外,vmcs还有一个区域是vm exit信息区域。这个区域是只读的,存储的是vmx指令失败后失败代码的编号。

在《处理器虚拟化技术》3.4章节中,展示了需要填充的字段以及其对应的ID。对于guest区域和host区域,需要填充的字段如下:

长度为16位的字段:


长度为64位的字段:


长度为32位的字段:

可以看到基本上都是进入guest区域之后的寄存器。其中对段寄存器的填充尤其麻烦,需要分别填充base、attribute、limit、selector。因此需要手动获取段寄存器并将其进行拆分然后进行相应的填充。根据保护模式的知识,段寄存器的结构大致如下:


在填充时需要将其拆分并填充,此步骤较为繁琐。

填充guest和host区域时还有四个重要的字段需要获取。分别是进入guest区域之后的rip和rsp以及从guest返回到host区域之后的rip rsp。这里我们要使得进入guest区域之后系统仍然正常进行,还是从原来的地方往下跑。因此要通过函数获取需要返回的上一层函数的返回地址以及rsp。

而对于返回host区域之后的host eip,由于从虚拟机中返回一定是发生了vmexit事件,需要对该事件进行处理,因此从虚拟机中返回之后的rip一定要设置为vmexit事件的处理函数。

而rsp则需要重新开辟一块内存区域供vmexit事件处理函数使用。如果还是使用guest返回之前的堆栈,则会破坏堆栈中的内容,导致无法预知的结果。

接下来开始具体字段的填充讲解。


vmxinit函数框架的搭建以及guest rip,rsp的获取方法


这里封装一个vmxinit函数用于进行vmon和vmcs区域的初始化。其中vmon区域的初始化较为简单,申请内存后将开头4个字节填充为IA32_VMX_BASE的后四字节,然后根据前一篇文章讲到的规则进行cr0,cr4的设置,最后通过vmxon指令进行开柜门即可。

详情看代码的实现,如果对其中操作有不清楚的地方可以参考上一篇文章。
int VmxInitVmOn(){ PVMXCPUPCB pVcpu = VmxGetCurrentCPUPCB();
PHYSICAL_ADDRESS lowphys,heiPhy;
lowphys.QuadPart = 0; heiPhy.QuadPart = -1;
pVcpu->VmxOnAddr = MmAllocateContiguousMemorySpecifyCache(PAGE_SIZE, lowphys, heiPhy, lowphys, MmCached);
if (!pVcpu->VmxOnAddr) { //申请内存失败 return -1; }
memset(pVcpu->VmxOnAddr, 0, PAGE_SIZE);
pVcpu->VmxOnAddrPhys = MmGetPhysicalAddress(pVcpu->VmxOnAddr);
//填充ID ULONG64 vmxBasic = __readmsr(IA32_VMX_BASIC);
*(PULONG)pVcpu->VmxOnAddr = (ULONG)vmxBasic;
//CR0,CR4
ULONG64 vcr00 = __readmsr(IA32_VMX_CR0_FIXED0); ULONG64 vcr01 = __readmsr(IA32_VMX_CR0_FIXED1);
ULONG64 vcr04 = __readmsr(IA32_VMX_CR4_FIXED0); ULONG64 vcr14 = __readmsr(IA32_VMX_CR4_FIXED1);
ULONG64 mcr4 = __readcr4(); ULONG64 mcr0 = __readcr0();
mcr4 |= vcr04; mcr4 &= vcr14;
mcr0 |= vcr00; mcr0 &= vcr01;
// __writecr0(mcr0);
__writecr4(mcr4);
int error = __vmx_on(&pVcpu->VmxOnAddrPhys.QuadPart);
if (error) { //释放内存,重置CR4 mcr4 &= ~vcr04; __writecr4(mcr4); MmFreeContiguousMemorySpecifyCache(pVcpu->VmxOnAddr, PAGE_SIZE, MmCached); pVcpu->VmxOnAddr = NULL; pVcpu->VmxOnAddrPhys.QuadPart = 0; }
return error;}

在初始化vmcs区域之前,首先要获取进入guest之后guest的rip以及rsp。由于我们希望在进入guest之后可以让虚拟机继续在我们进入guest之前的代码上执行,因此我们要获取vmxinit函数的返回地址以及堆栈中保存的上一层函数的rsp。

在进入到guest之后直接从vmxinit函数的返回地址开始跑,并将堆栈设置为上一层函数的堆栈。这里要使用到一个vs的内嵌函数 _AddressOfReturnAddress 。这个函数在编译时会返回指向堆栈中上一层函数的返回地址的指针。

因此通过这个函数可以得到指向需要使用的rip的指针。而rip存储的位置往下8个字节便是上一层函数的rsp。

因此获取guest函数的rip和rsp的代码如下:
PULONG64 retAddr = (PULONG64)_AddressOfReturnAddress();ULONG64 guestEsp = retAddr + 1;ULONG64 guestEip = *retAddr;

因此,vmxinit函数的大体框架如下。其中hosteip传入的是vmexit的处理函数的地址。在发生vmexit事件之后跳到vmexit处理函数中进行相应处理。
int VmxInit(ULONG64 hostEip){
PVMXCPUPCB pVcpu = VmxGetCurrentCPUPCB();
pVcpu->cpuNumber = KeGetCurrentProcessorNumberEx(NULL);
PULONG64 retAddr = (PULONG64)_AddressOfReturnAddress(); ULONG64 guestEsp = retAddr + 1; ULONG64 guestEip = *retAddr;
int error = VmxInitVmOn();
if (error) { DbgPrintEx(77, 0, "[db]:vmon 初始化失败 error = %d,cpunumber %d\r\n", error, pVcpu->cpuNumber);
return error; }
error = VmxInitVmcs(guestEip, guestEsp, hostEip);
if (error) { DbgPrintEx(77, 0, "[db]:vmcs 初始化失败 error = %d,cpunumber %d\r\n", error, pVcpu->cpuNumber);

VmxDestory(); return error; }
return 0;}


设置vmcs字段的vmxinitvmcs函数解析


VmxInitVmcs 函数中进行对vmcs的设置。
int VmxInitVmcs(ULONG64 GuestEip,ULONG64 GuestEsp, ULONG64 hostEip){ PVMXCPUPCB pVcpu = VmxGetCurrentCPUPCB();
PHYSICAL_ADDRESS lowphys, heiPhy;
lowphys.QuadPart = 0; heiPhy.QuadPart = -1;
pVcpu->VmxcsAddr = MmAllocateContiguousMemorySpecifyCache(PAGE_SIZE, lowphys, heiPhy, lowphys, MmCached);
if (!pVcpu->VmxcsAddr) { //申请内存失败 return -1; }
memset(pVcpu->VmxcsAddr, 0, PAGE_SIZE);
pVcpu->VmxcsAddrPhys = MmGetPhysicalAddress(pVcpu->VmxcsAddr);

pVcpu->VmxHostStackTop = MmAllocateContiguousMemorySpecifyCache(PAGE_SIZE * 36, lowphys, heiPhy, lowphys, MmCached);
if (!pVcpu->VmxHostStackTop) { //申请内存失败
return -1; }
memset(pVcpu->VmxHostStackTop, 0, PAGE_SIZE * 36);
pVcpu->VmxHostStackBase = (ULONG64)pVcpu->VmxHostStackTop + PAGE_SIZE * 36 - 0x200;
//填充ID ULONG64 vmxBasic = __readmsr(IA32_VMX_BASIC);
*(PULONG)pVcpu->VmxcsAddr = (ULONG)vmxBasic;
//加载VMCS __vmx_vmclear(&pVcpu->VmxcsAddrPhys.QuadPart);
__vmx_vmptrld(&pVcpu->VmxcsAddrPhys.QuadPart);
VmxInitGuest(GuestEip, GuestEsp);
VmxInitHost(hostEip);}

和设置vmon区域类似,首先也是申请内存区域然后填充IA32_VMX_BASIC。而在进行基本ID的填充之后,要通过vmclear初始化内存并通过vmptrld选择vmcs区域。这两步操作对应的是前一篇文章说的拔电源以及选中机器。

在完成之后,便是最复杂的vmcs字段的填充。这里对于每一块vmcs字段,分别封装一个函数进行初始化。

本篇文章讨论的是guest区域和host区域的初始化,分别对应的是 VmxInitGuest 和 VmxInitHost 函数。



GDT表项属性的分离与填充(es cs ss ds fs gs ldtr)


对于guest相关的字段,需要传入的是guesteip和guestesp,以用来确定在进入guest虚拟机之后guest从哪里开始跑。其他的字段全部根据当前状态填入。

首先需要填入的便是gdt表中各个段寄存器的base、limit、attribute、selector。在对其ID进行观察后可以发现,这些字段的id都是连在一起的,id的值相差2。而对于这些段寄存器,分离base、limit、attribute、selector的方法也非常类似。

因此可以考虑将填写段寄存器的属性的方法封装成一个函数。这里将其封装成 fillGdtDataItem 函数。对于各个属性的分离,依照下图来进行。

具体分离的细节不赘述,建议仔细读懂代码中切割bit的方法。


void fillGdtDataItem(int index,short selector){ GdtTable gdtTable = {0}; AsmGetGdtTable(&gdtTable);
selector &= 0xFFF8;
ULONG limit = __segmentlimit(selector); PULONG item = (PULONG)(gdtTable.Base + selector);
LARGE_INTEGER itemBase = {0}; itemBase.LowPart = (*item & 0xFFFF0000) >> 16; item += 1; itemBase.LowPart |= (*item & 0xFF000000) | ((*item & 0xFF) << 16);
//属性 ULONG attr = (*item & 0x00F0FF00) >> 8;
if (selector == 0) { attr |= 1 << 16; }
__vmx_vmwrite(GUEST_ES_BASE + index * 2, itemBase.QuadPart); __vmx_vmwrite(GUEST_ES_LIMIT + index * 2, limit); __vmx_vmwrite(GUEST_ES_AR_BYTES + index * 2, attr); __vmx_vmwrite(GUEST_ES_SELECTOR + index * 2, selector);}



tr寄存器gdt表项的填充


对于tr寄存器的gdt表项,并不能像其他寄存器一样进行填充。因为在64位下,tr寄存器的gdt表项是128位的。因此需要单独设置。64位下tr寄存器的gdt表项格式在intel白皮书的7.2.3章节中有解析。其结构如下:
因此其需要用另外一套代码进行设置,具体代码如下。思路与其他gdt表项的设置思路相同,都是取出对应的位填入vmcs区域中。
GdtTable gdtTable = { 0 };AsmGetGdtTable(&gdtTable);
ULONG trSelector = AsmReadTR();
trSelector &= 0xFFF8;ULONG trlimit = __segmentlimit(trSelector);
LARGE_INTEGER trBase = {0};
PULONG trItem = (PULONG)(gdtTable.Base + trSelector);
//读TRtrBase.LowPart = ((trItem[0] >> 16) & 0xFFFF) | ((trItem[1] & 0xFF) << 16) | ((trItem[1] & 0xFF000000));trBase.HighPart = trItem[2];
//属性ULONG attr = (trItem[1] & 0x00F0FF00) >> 8;__vmx_vmwrite(GUEST_TR_BASE, trBase.QuadPart);__vmx_vmwrite(GUEST_TR_LIMIT, trlimit);__vmx_vmwrite(GUEST_TR_AR_BYTES, attr);__vmx_vmwrite(GUEST_TR_SELECTOR, trSelector);


guest区域中其他寄存器的填充


接下来是其他一些特殊寄存器的填充。这里不具体深入说。但是其实这里一些特殊寄存器的特殊属性可以用来做虚拟机检测。

在进行一些特殊的操作之后可以使得在host状态下和guest状态下的结果不一样,从而检测到VT的存在。

例如在之后的msr设置中,如果在guest中尝试读取一个超出msr范围的寄存器,在真机中会抛错,但是在虚拟机中不具体处理的话会产生不可预料的结果。

虽然intel保证无法在guest中检测到自身是否为guest,但是还是可以通过很多方法进行相应的检测。反虚拟机和反反虚拟机都要求开发者对CPU的各种行为有很深入的理解。
__vmx_vmwrite(GUEST_CR0, __readcr0());__vmx_vmwrite(GUEST_CR4, __readcr4());__vmx_vmwrite(GUEST_CR3, __readcr3());__vmx_vmwrite(GUEST_DR7, __readdr(7));__vmx_vmwrite(GUEST_RFLAGS, __readeflags());__vmx_vmwrite(GUEST_RSP, GuestEsp);__vmx_vmwrite(GUEST_RIP, GuestEip);
__vmx_vmwrite(VMCS_LINK_POINTER, -1);
__vmx_vmwrite(GUEST_IA32_DEBUGCTL, __readmsr(IA32_MSR_DEBUGCTL));__vmx_vmwrite(GUEST_IA32_PAT, __readmsr(IA32_MSR_PAT));__vmx_vmwrite(GUEST_IA32_EFER, __readmsr(IA32_MSR_EFER));
__vmx_vmwrite(GUEST_FS_BASE, __readmsr(IA32_FS_BASE));__vmx_vmwrite(GUEST_GS_BASE, __readmsr(IA32_GS_BASE));
__vmx_vmwrite(GUEST_SYSENTER_CS, __readmsr(0x174));__vmx_vmwrite(GUEST_SYSENTER_ESP, __readmsr(0x175));__vmx_vmwrite(GUEST_SYSENTER_EIP, __readmsr(0x176));

//IDT GDT
GdtTable idtTable;__sidt(&idtTable);
__vmx_vmwrite(GUEST_GDTR_BASE, gdtTable.Base);__vmx_vmwrite(GUEST_GDTR_LIMIT, gdtTable.limit);__vmx_vmwrite(GUEST_IDTR_LIMIT, idtTable.limit);__vmx_vmwrite(GUEST_IDTR_BASE, idtTable.Base);


guest区域填充完整代码

void fillGdtDataItem(int index,short selector){ GdtTable gdtTable = {0}; AsmGetGdtTable(&gdtTable);
selector &= 0xFFF8;
ULONG limit = __segmentlimit(selector); PULONG item = (PULONG)(gdtTable.Base + selector);
LARGE_INTEGER itemBase = {0}; itemBase.LowPart = (*item & 0xFFFF0000) >> 16; item += 1; itemBase.LowPart |= (*item & 0xFF000000) | ((*item & 0xFF) << 16);
//属性 ULONG attr = (*item & 0x00F0FF00) >> 8;
if (selector == 0) { attr |= 1 << 16; }
__vmx_vmwrite(GUEST_ES_BASE + index * 2, itemBase.QuadPart); __vmx_vmwrite(GUEST_ES_LIMIT + index * 2, limit); __vmx_vmwrite(GUEST_ES_AR_BYTES + index * 2, attr); __vmx_vmwrite(GUEST_ES_SELECTOR + index * 2, selector);}
void VmxInitGuest(ULONG64 GuestEip, ULONG64 GuestEsp){ fillGdtDataItem(0, AsmReadES()); fillGdtDataItem(1, AsmReadCS()); fillGdtDataItem(2, AsmReadSS()); fillGdtDataItem(3, AsmReadDS()); fillGdtDataItem(4, AsmReadFS()); fillGdtDataItem(5, AsmReadGS()); fillGdtDataItem(6, AsmReadLDTR());
GdtTable gdtTable = { 0 }; AsmGetGdtTable(&gdtTable);
ULONG trSelector = AsmReadTR();
trSelector &= 0xFFF8; ULONG trlimit = __segmentlimit(trSelector);
LARGE_INTEGER trBase = {0};
PULONG trItem = (PULONG)(gdtTable.Base + trSelector);

//读TR trBase.LowPart = ((trItem[0] >> 16) & 0xFFFF) | ((trItem[1] & 0xFF) << 16) | ((trItem[1] & 0xFF000000)); trBase.HighPart = trItem[2];
//属性 ULONG attr = (trItem[1] & 0x00F0FF00) >> 8; __vmx_vmwrite(GUEST_TR_BASE, trBase.QuadPart); __vmx_vmwrite(GUEST_TR_LIMIT, trlimit); __vmx_vmwrite(GUEST_TR_AR_BYTES, attr); __vmx_vmwrite(GUEST_TR_SELECTOR, trSelector);
__vmx_vmwrite(GUEST_CR0, __readcr0()); __vmx_vmwrite(GUEST_CR4, __readcr4()); __vmx_vmwrite(GUEST_CR3, __readcr3()); __vmx_vmwrite(GUEST_DR7, __readdr(7)); __vmx_vmwrite(GUEST_RFLAGS, __readeflags()); __vmx_vmwrite(GUEST_RSP, GuestEsp); __vmx_vmwrite(GUEST_RIP, GuestEip);
__vmx_vmwrite(VMCS_LINK_POINTER, -1);
__vmx_vmwrite(GUEST_IA32_DEBUGCTL, __readmsr(IA32_MSR_DEBUGCTL)); __vmx_vmwrite(GUEST_IA32_PAT, __readmsr(IA32_MSR_PAT)); __vmx_vmwrite(GUEST_IA32_EFER, __readmsr(IA32_MSR_EFER));
__vmx_vmwrite(GUEST_FS_BASE, __readmsr(IA32_FS_BASE)); __vmx_vmwrite(GUEST_GS_BASE, __readmsr(IA32_GS_BASE));
__vmx_vmwrite(GUEST_SYSENTER_CS, __readmsr(0x174)); __vmx_vmwrite(GUEST_SYSENTER_ESP, __readmsr(0x175)); __vmx_vmwrite(GUEST_SYSENTER_EIP, __readmsr(0x176));

//IDT GDT
GdtTable idtTable; __sidt(&idtTable);
__vmx_vmwrite(GUEST_GDTR_BASE, gdtTable.Base); __vmx_vmwrite(GUEST_GDTR_LIMIT, gdtTable.limit); __vmx_vmwrite(GUEST_IDTR_LIMIT, idtTable.limit); __vmx_vmwrite(GUEST_IDTR_BASE, idtTable.Base);
}



host区域填充


host区域填充的内容和guest填充的内容类似。注意host中的gdt表项并不需要填充各项属性,只需要填充selector,另一个需要注意的点是host的rsp必须使用自己申请的一块内存。

如果还是使用guest退出时的rsp,一定会导致guest中堆栈被破坏从而导致不可预知的结果。host区域填充的代码如下:
void VmxInitHost(ULONG64 HostEip){ GdtTable gdtTable = { 0 }; AsmGetGdtTable(&gdtTable);
PVMXCPUPCB pVcpu = VmxGetCurrentCPUPCB();
ULONG trSelector = AsmReadTR();
trSelector &= 0xFFF8;
LARGE_INTEGER trBase = { 0 };
PULONG trItem = (PULONG)(gdtTable.Base + trSelector);

//读TR trBase.LowPart = ((trItem[0] >> 16) & 0xFFFF) | ((trItem[1] & 0xFF) << 16) | ((trItem[1] & 0xFF000000)); trBase.HighPart = trItem[2];
//属性 __vmx_vmwrite(HOST_TR_BASE, trBase.QuadPart); __vmx_vmwrite(HOST_TR_SELECTOR, trSelector);
__vmx_vmwrite(HOST_ES_SELECTOR, AsmReadES() & 0xfff8); __vmx_vmwrite(HOST_CS_SELECTOR, AsmReadCS() & 0xfff8); __vmx_vmwrite(HOST_SS_SELECTOR, AsmReadSS() & 0xfff8); __vmx_vmwrite(HOST_DS_SELECTOR, AsmReadDS() & 0xfff8); __vmx_vmwrite(HOST_FS_SELECTOR, AsmReadFS() & 0xfff8); __vmx_vmwrite(HOST_GS_SELECTOR, AsmReadGS() & 0xfff8);


__vmx_vmwrite(HOST_CR0, __readcr0()); __vmx_vmwrite(HOST_CR4, __readcr4()); __vmx_vmwrite(HOST_CR3, __readcr3()); __vmx_vmwrite(HOST_RSP, (ULONG64)pVcpu->VmxHostStackBase); __vmx_vmwrite(HOST_RIP, HostEip);

__vmx_vmwrite(HOST_IA32_PAT, __readmsr(IA32_MSR_PAT)); __vmx_vmwrite(HOST_IA32_EFER, __readmsr(IA32_MSR_EFER));
__vmx_vmwrite(HOST_FS_BASE, __readmsr(IA32_FS_BASE)); __vmx_vmwrite(HOST_GS_BASE, __readmsr(IA32_GS_KERNEL_BASE));
__vmx_vmwrite(HOST_IA32_SYSENTER_CS, __readmsr(0x174)); __vmx_vmwrite(HOST_IA32_SYSENTER_ESP, __readmsr(0x175)); __vmx_vmwrite(HOST_IA32_SYSENTER_EIP, __readmsr(0x176));

//IDT GDT
GdtTable idtTable; __sidt(&idtTable);
__vmx_vmwrite(HOST_GDTR_BASE, gdtTable.Base); __vmx_vmwrite(HOST_IDTR_BASE, idtTable.Base);}

下一篇文章将会讲解vm-control fields的填充。本篇文章的代码稍晚会传到github仓库中。


参考文献

intel白皮书

邓志《处理器虚拟化技术》

B站周壑VT教学视频

https://github.com/qq1045551070/VtToMe

火哥上课讲的内容





看雪ID:smallzhong_

https://bbs.pediy.com/user-home-899076.htm

*本文由看雪论坛 smallzhong_ 原创,转载请注明来自看雪社区


# 往期推荐

1.通过DWARF Expression将代码隐藏在栈展开过程中

2.x86-页式管理

3.HG532e漏洞复现(cve-2017-17215)

4.Android加壳脱壳学习—动态加载和类加载机制详解

5.CVE-2021-26411漏洞分析笔记

6.Windows本地提权漏洞CVE-2014-1767分析及EXP编写指导






球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存